import socket
import json
import time
import uuid
import os
import sys
import threading
import subprocess
import psutil
import shutil
from datetime import datetime
from queue import Queue
from typing import Dict, Any, List
import uno
from com.sun.star.beans import PropertyValue


soffice_tmp = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "soffice")
shutil.rmtree(soffice_tmp, ignore_errors=True)
os.makedirs(soffice_tmp, exist_ok=True)
os.chmod(soffice_tmp, 0o777)




class SimpleSocketServer:
    def __init__(self, portlist: List[int], host: str = '0.0.0.0', port: int = 5005, max_memory_mb: int = 20):
        self.portlist = portlist
        self.host = host
        self.port = port
        self.max_memory_mb = max_memory_mb
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        
        # 连接池存储端口和对应的连接上下文
        self.connection_pool = Queue(len(portlist))
        
        # 存储每个端口的进程信息
        self.soffice_processes = {}
        self.chcaepids = {}
        
        # 启动所有 LibreOffice 实例
        self.start_all_soffice_instances()
        
        # 初始化连接
        self.init_connections()

    def check_and_kill_port_occupant(self, port: int) -> bool:
        """检查端口是否被占用，如被占用则杀死对应进程（核心新增功能）"""
        try:
            # 遍历所有TCP连接，查找占用目标端口的进程
            for conn in psutil.net_connections(kind='tcp'):
                if conn.laddr.port == port:  # 匹配本地端口
                    pid = conn.pid
                    if pid is None:
                        print(f"警告：端口 {port} 被占用，但无法获取进程PID（可能无权限）")
                        return False
                    
                    try:
                        # 获取占用进程信息
                        process = psutil.Process(pid)
                        process_name = process.name()
                        print(f"发现端口 {port} 被进程占用: {process_name} (PID: {pid})")
                        
                        # 终止进程及其所有子进程
                        for child in process.children(recursive=True):
                            child.terminate()
                        process.terminate()
                        
                        # 等待进程终止（最多5秒）
                        try:
                            process.wait(timeout=5)
                            print(f"成功终止进程 {process_name} (PID: {pid})")
                        except subprocess.TimeoutExpired:
                            # 终止失败，强制杀死
                            for child in process.children(recursive=True):
                                child.kill()
                            process.kill()
                            print(f"强制杀死进程 {process_name} (PID: {pid})")
                        
                        return True  # 端口已释放
                        
                    except psutil.NoSuchProcess:
                        print(f"进程 (PID: {pid}) 已退出，端口 {port} 自动释放")
                        return True
                    except psutil.AccessDenied:
                        print(f"错误：无权限终止进程 (PID: {pid})，请手动释放端口 {port}")
                        return False
            
            # 端口未被占用
            print(f"端口 {port} 未被占用")
            return True
            
        except Exception as e:
            print(f"检查端口占用时出错: {str(e)}")
            return False

    def is_port_listening(self, port: int, host: str = 'localhost', timeout: int = 1) -> bool:
        """检查端口是否处于监听状态（辅助功能）"""
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(timeout)
                return s.connect_ex((host, port)) == 0  # 0表示端口可连接
        except Exception:
            return False

    def get_child_pid(self, parent_pid):
        if parent_pid in self.chcaepids:
            return self.chcaepids[parent_pid]
        """通过父进程PID查找子进程PID"""
        try:
            # ps -o pid --no-headers --ppid 父PID：列出父进程的所有子进程PID
            result = subprocess.run(
                ["ps", "-o", "pid", "--no-headers", "--ppid", str(parent_pid)],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                check=True
            )
            # 输出可能有多个子进程（如有），取第一个非空PID
            child_pids = [pid.strip() for pid in result.stdout.split() if pid.strip()]
            if child_pids:
                self.chcaepids[parent_pid] = int(child_pids[0])
            return int(child_pids[0]) if child_pids else None
        except subprocess.CalledProcessError:
            return None

    def start_soffice_instance(self, port: int):
        #if not self.check_and_kill_port_occupant(port):
        #    print(f"端口 {port} 清理失败，无法启动实例")
        #    return False

        envdir = os.path.join(soffice_tmp, str(uuid.uuid4()))
        os.makedirs(envdir, exist_ok=True)
        os.chmod(envdir, 0o777)
        envdir = envdir.replace('\\', '/')
        """启动单个 LibreOffice 实例"""
        try:
            # 构建启动命令
            cmd = [
                "soffice",
                "--headless",
                "--invisible",
                "--nologo",
                "--norestore",
                "--nodefault",
                f"--accept=socket,host=localhost,port={port};urp;StarOffice.ServiceManager",
                f"-env:UserInstallation=file:///{envdir}"
            ]
            # 启动进程
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                start_new_session=True  # 确保进程在独立会话中运行
            )
            
            # 检查端口是否监听（最多等待30秒）
            max_wait_seconds = 30
            check_interval = 2
            start_time = time.time()
            port_listening = False
            while time.time() - start_time < max_wait_seconds:
                if self.is_port_listening(port):
                    port_listening = True
                    break
                time.sleep(check_interval)
            
            if not port_listening:
                process.terminate()
                stdout, stderr = process.communicate()
                print(f"端口 {port} 启动失败，日志：")
                print("STDOUT:", stdout.decode('utf-8', errors='ignore'))
                print("STDERR:", stderr.decode('utf-8', errors='ignore'))
                return False
            
            # 存储进程信息
            self.soffice_processes[port] = {
                "process": process,
                "start_time": time.time(),
                "restart_count": 0
            }
            
            print(f"启动 LibreOffice 实例，端口: {port}, PID: {process.pid}")
            return True
            
        except Exception as e:
            print(f"启动 LibreOffice 实例失败 (端口: {port}): {e}")
            return False

    def start_all_soffice_instances(self):
        """启动所有 LibreOffice 实例"""
        for port in self.portlist:
            self.start_soffice_instance(port)

    def kill_soffice_instance(self, port: int):
        """终止指定端口的 LibreOffice 实例"""
        if port not in self.soffice_processes:
            return False
            
        try:
            # 获取存储的进程对象
            process_info = self.soffice_processes[port]
            process = process_info["process"]
            parent_pid = process.pid
            # 1. 先检查进程是否已退出
            if process.poll() is not None:
                print(f"端口 {port} 的进程 (PID: {process.pid}) 已退出，无需终止")
                del self.soffice_processes[port]
                del self.chcaepids[parent_pid]
                return True
            
            # 2. 进程存在，尝试终止（使用psutil处理进程树）
            try:
                parent = psutil.Process(process.pid)
            except psutil.NoSuchProcess:
                print(f"端口 {port} 的进程 (PID: {process.pid}) 已不存在（可能已被手动终止）")
                del self.soffice_processes[port]
                del self.chcaepids[parent_pid]
                return True
            except psutil.AccessDenied:
                print(f"无权限访问进程 (PID: {process.pid})，无法终止端口 {port} 的实例")
                return False
            
            # 3. 终止所有子进程和父进程
            try:
                # 先终止子进程
                for child in parent.children(recursive=True):
                    child.terminate()
                # 再终止父进程
                parent.terminate()
                print(f"已发送终止信号给进程 (PID: {process.pid}) 及其子进程")
                
                # 等待进程退出（最多5秒）
                parent.wait(timeout=5)
                print(f"进程 (PID: {process.pid}) 已正常终止")
                
            except subprocess.TimeoutExpired:
                # 终止超时，强制杀死
                for child in parent.children(recursive=True):
                    child.kill()
                parent.kill()
                print(f"进程 (PID: {process.pid}) 终止超时，已强制杀死")
            except psutil.NoSuchProcess:
                print(f"进程 (PID: {process.pid}) 在终止过程中已退出")
            except Exception as e:
                print(f"终止进程时出错: {type(e)} {e}")
                return False
                
            del self.soffice_processes[port]
            del self.chcaepids[parent_pid]
            print(f"已终止 LibreOffice 实例，端口: {port}")
            return True
            
        except Exception as e:
            print(f"终止 LibreOffice 实例失败 (端口: {port}): {e}")
            return False

    def get_soffice_memory_usage(self, port: int) -> float:
        """获取指定端口 LibreOffice 实例的内存使用量(MB)"""
        if port not in self.soffice_processes:
            return 0
            
        try:
            process = self.soffice_processes[port]["process"]
            ps_process = psutil.Process(self.get_child_pid(process.pid))
            memory_mb = ps_process.memory_info().rss / (1024 * 1024)
            return memory_mb
        except:
            return 0

    def restart_soffice_instance(self, port: int):
        """重启指定端口的LibreOffice实例（用于线程执行）"""
        try:
            print(f"线程开始重启端口 {port} 的实例...")
            # 终止当前实例
            self.kill_soffice_instance(port)
            
            # 等待片刻
            time.sleep(2)
            
            # 重新启动实例
            success = self.start_soffice_instance(port)
            
            if success:
                # 等待实例启动完成
                time.sleep(5)
                
                # 创建新连接并放回连接池
                new_ctx = self.create_connection(port)
                if new_ctx:
                    self.connection_pool.put((port, new_ctx))
                    return {'code': 200,'msg': 'Success','result': f"端口 {port} 重启完成并已放回连接池"}
                else:
                    return {'code': 203,'msg': 'Error','result': f"端口 {port} 重启成功但无法创建连接"}
            else:
                return {'code': 204,'msg': 'Error','result': f"soffice端口 {port} 重启失败"}
                
        except Exception as e:
            return {'code': 205,'msg': 'Error','result': f"重启实例线程出错: {e}"}

    def check_and_restart_if_needed(self, port: int):
        """检查内存使用情况，如果需要则重启实例"""
        memory_usage = self.get_soffice_memory_usage(port)
        
        if memory_usage > self.max_memory_mb:
            print(f"端口 {port} 的内存使用量 {memory_usage:.2f}MB 超过阈值 {self.max_memory_mb}MB，准备重启...")
            
            restart_thread = threading.Thread(
                target=self.restart_soffice_instance,
                args=(port,),
                daemon=True  # 设置为守护线程，确保程序退出时线程也会退出
            )
            restart_thread.start()
            return True
            
        return None

    def create_connection(self, port: int):
        """创建到指定端口 LibreOffice 实例的连接"""
        try:
            local_context = uno.getComponentContext()
            smgr = local_context.getServiceManager()
            resolver = smgr.createInstanceWithContext(
                "com.sun.star.bridge.UnoUrlResolver", local_context)
            ctx = resolver.resolve(
                f"uno:socket,host=localhost,port={port};urp;StarOffice.ServiceManager")
            return ctx
        except Exception as e:
            print(f"创建连接到端口 {port} 失败: {e}")
            return None

    def init_connections(self):
        """初始化所有连接"""
        retries = 3  # 最多重试3次
        for port in self.portlist:
            ctx = None
            for _ in range(retries):
                ctx = self.create_connection(port)
                if ctx:
                    break
                print(f"连接端口 {port} 失败，重试中...")
                time.sleep(5)  # 重试间隔5秒
            if ctx:
                self.connection_pool.put((port, ctx))
                print(f"已连接端口 {port}")
            else:
                print(f"端口 {port} 多次连接失败，可能启动异常")

    def get_process_cpu_relative(self, port):
        if port not in self.soffice_processes:
            return 0
        try:
            process = self.soffice_processes[port]["process"]
            ps_process = psutil.Process(self.get_child_pid(process.pid))
            process_cpu = ps_process.cpu_percent(interval=0.5)
            relative_cpu = process_cpu / psutil.cpu_count()
            return relative_cpu
        except psutil.NoSuchProcess:
            return 0

    def get_connection_status(self):
        statusall = {}
        for port in self.portlist:
            if port not in self.soffice_processes:
                continue
            svs = self.soffice_processes[port]
            statusall[port] = {
                "memory": round(self.get_soffice_memory_usage(port), 2),
                "cpu": self.get_process_cpu_relative(port),
                "pid": self.chcaepids.get(port, svs["process"].pid),
                "time": datetime.fromtimestamp(svs["start_time"]).strftime("%Y-%m-%d %H:%M:%S")
            }
        return statusall

    def get_connection(self):
        """从连接池获取一个连接"""
        while True:  # 添加循环确保获取到有效连接
            
            if self.connection_pool.empty():
                print("连接池为空，等待可用连接...")
                time.sleep(1)
                continue
                
            port, ctx = self.connection_pool.get()
            #print("获取链接对象", port, ctx)
            # 判断连接是否有效
            try:
                # 简单测试连接是否有效
                #smgr = ctx.getServiceManager()
                #if smgr:
                # 检查内存使用情况
                if self.check_and_restart_if_needed(port):
                    # 已启动重启线程，继续获取下一个连接
                    continue
                return port, ctx
            except: 
                print(f"端口 {port} 的连接无效，将被丢弃")
            
            # 如果连接无效，尝试创建新区连接并继续循环获取
            
            new_ctx = self.create_connection(port)
            
            if new_ctx:
                self.connection_pool.put((port, new_ctx)) 
                print(f("端口 {port} 已重新创建连接"))
                
            continue

    def release_connection(self, port: int, ctx):
        """将连接释放回连接池"""
        self.connection_pool.put((port, ctx))

    def validate_request(self, data: Dict[str, Any]) -> Dict[str, Any]:
        """验证请求数据"""
        required_fields = ['model']
        for field in required_fields:
            if field not in data:
                return {'code': 400, 'msg': f'Missing required field: {field}'}
        
        # 检查文件是否存在
        #if not os.path.exists(data['inputValue']):
        #    return {'code': 400, 'msg': f'Input file does not exist: {data["inputValue"]}'}
        
        return {'code': 200, 'msg': 'Validation passed'}

    def process_request(self, data: Dict[str, Any]) -> Dict[str, Any]:
        """处理请求数据"""
        try:
            timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
            
            # 获取连接
            port, ctx = self.get_connection()
            
            # 定义保存属性
            props = (
                PropertyValue(Name="Overwrite", Value=True),
                PropertyValue(Name="FilterName", Value="Calc MS Excel 2007 XML"),
                PropertyValue(Name="FilterOptions", Value="MS Excel 2007 XML")
            )
            
            # 执行转换
            desktop = ctx.createInstanceWithContext("com.sun.star.frame.Desktop", uno.getComponentContext())
            doc = desktop.loadComponentFromURL(uno.systemPathToFileUrl(data["inputValue"]), "_blank", 0, ())
            doc.storeToURL(uno.systemPathToFileUrl(data["outValue"]), props)
            doc.close(True)
            
            # 释放连接
            self.release_connection(port, ctx)
            
            return {
                'code': 200,
                'msg': 'Success',
                'result': data["outValue"]
            }
            
        except Exception as e:
            print("处理错误", e)
            return {'code': 500, 'msg': f'Processing error: {str(e)}'}

    def handle_client(self, client_socket):
        """处理客户端连接"""
        try:
            raw_data = client_socket.recv(1024).decode('utf-8')
            if not raw_data:
                return
            
            try:
                data = json.loads(raw_data)
                validation_result = self.validate_request(data)
                if validation_result['code'] != 200:
                    response = validation_result
                else:
                    if data["model"] == "status":
                        response = self.get_connection_status()
                    elif data["model"] == "restart":
                        response = self.restart_soffice_instance(data["value"])
                    else:
                        response = self.process_request(data)
                
            except json.JSONDecodeError:
                response = {'code': 400, 'msg': 'Invalid JSON format'}
        except Exception as e:
            print(f"Error handling client: {e}")
            response = {'code': 500, 'msg': f'Server error: {str(e)}'}
        finally:
            client_socket.send(json.dumps(response).encode('utf-8'))
            client_socket.close()

    def start(self):
        """启动服务器"""
        self.server_socket.bind((self.host, self.port))
        self.server_socket.listen(5)
        print(f"开启服务: {self.host}:{self.port}")
        
        try:
            while True:
                client_socket, addr = self.server_socket.accept()
                #print(f"接收到连接: {addr}")
                client_thread = threading.Thread(
                    target=self.handle_client,
                    args=(client_socket,),
                    daemon=True  # 设为守护线程，服务器退出时自动结束
                )
                client_thread.start()
        except KeyboardInterrupt:
            print("服务器被用户中断")
        except Exception as e:
            print("服务器错误", e)
        finally:
            # 清理所有 LibreOffice 实例
            for port in list(self.soffice_processes.keys()):
                self.kill_soffice_instance(port)
            self.server_socket.close()
            print("服务器已关闭")



if __name__ == "__main__":
    # 可以根据需要调整端口列表和最大内存限制
    server = SimpleSocketServer(
        portlist=[9002, 9003, 9004],  # 三个实例端口
        port=5005,                    # 服务器端口
        max_memory_mb=1600            # 最大内存限制(MB)
    )
    server.start()